<?php
//启动服务
(new ws())->run();
/**
 * 原生PHP的websocket服务
 */
class ws
{
    protected string $address = '0.0.0.0';
    protected int $port = 3233;
    protected Socket $_socket;
    protected array $users = [];//在线用户数
    protected $redis = null; // Redis连接实例
    protected string $chatHistoryKey = 'chat_history'; // Redis中存储聊天记录的key
    protected int $lastRedisPingTime = 0; // 上次Redis心跳时间

    /**
     * 构造函数，定义listen IP和Port
     * @param string $address
     * @param int $port
     */
    public function __construct(string $address = '', int $port = 0)
    {
        !empty($address) && $this->address = $address;
        $port > 0 && $this->port = $port;
        
        // 初始化Redis连接
        $this->initRedis();
    }
    
    /**
     * 检查Redis连接并执行心跳
     */
    protected function checkRedisConnection(): void
    {
        if (!$this->redis) {
            return;
        }
        
        // 每60秒执行一次Redis心跳
        $currentTime = time();
        if ($currentTime - $this->lastRedisPingTime >= 60) {
            try {
                // 执行PING命令作为心跳
                $result = $this->redis->ping();
                if ($result === false || $result === null) {
                    echo date('Y-m-d H:i:s') . " Redis心跳失败，尝试重新连接\n";
                    $this->initRedis(); // 尝试重新连接
                } else {
                    echo date('Y-m-d H:i:s') . " Redis心跳成功: " . ($result === true ? "true" : $result) . "\n";
                }
            } catch (Exception $e) {
                echo date('Y-m-d H:i:s') . " Redis心跳异常: " . $e->getMessage() . "，尝试重新连接\n";
                $this->initRedis(); // 尝试重新连接
            }
            
            $this->lastRedisPingTime = $currentTime;
        }
    }

    /**
     * 初始化Redis连接
     */
    protected function initRedis(): void
    {
        try {
            if (class_exists('Redis')) {
                $redis = new Redis();
                $redis->connect('127.0.0.1', 6379);
                
                // Redis密码验证 - 如果有密码请修改此处
                $redisPassword = ''; // 这里填写Redis密码，无密码保留为空
                if (!empty($redisPassword)) {
                    $authResult = $redis->auth($redisPassword);
                    if (!$authResult) {
                        echo date('Y-m-d H:i:s') . " Redis密码验证失败\n";
                        $this->redis = null;
                        return;
                    }
                    echo date('Y-m-d H:i:s') . " Redis密码验证成功\n";
                }
                
                $this->redis = $redis;
                $this->lastRedisPingTime = time(); // 重置心跳时间
                echo date('Y-m-d H:i:s') . " Redis连接成功\n";
            } else {
                echo date('Y-m-d H:i:s') . " Redis扩展未安装\n";
                $this->redis = null;
            }
        } catch (Exception $e) {
            echo date('Y-m-d H:i:s') . " Redis连接失败: " . $e->getMessage() . "\n";
            $this->redis = null;
        }
    }
    
    /**
     * 添加消息到Redis历史记录
     * @param string $message 格式化后的消息
     */
    protected function addMessageToHistory(string $message): void
    {
        if (!$this->redis) return;
        
        try {
            // 将消息添加到列表头部
            $this->redis->lPush($this->chatHistoryKey, $message);
            // 保持最多2048条消息
            $this->redis->lTrim($this->chatHistoryKey, 0, 2047);
        } catch (Exception $e) {
            echo date('Y-m-d H:i:s') . " Redis存储消息失败: " . $e->getMessage() . "\n";
        }
    }
    
    /**
     * 获取历史消息
     * @param int $start 起始位置
     * @param int $count 获取数量
     * @return array 消息列表
     */
    protected function getHistoryMessages(int $start, int $count): array
    {
        if (!$this->redis) return [];
        
        try {
            // 获取消息总数
            $totalMessages = $this->redis->lLen($this->chatHistoryKey);
            echo date('Y-m-d H:i:s') . " 获取历史消息 - 总消息数: {$totalMessages}, 请求: start={$start}, count={$count}\n";
            
            // 如果请求的起始位置超出范围，返回空数组
            if ($start >= $totalMessages) {
                echo date('Y-m-d H:i:s') . " 获取历史消息 - 起始位置超出范围\n";
                return [];
            }
            
            // 从Redis获取指定范围的消息
            $messages = $this->redis->lRange($this->chatHistoryKey, $start, $start + $count - 1);
            echo date('Y-m-d H:i:s') . " 获取历史消息 - 成功获取: " . count($messages) . " 条消息\n";
            
            return $messages;
        } catch (Exception $e) {
            echo date('Y-m-d H:i:s') . " Redis获取消息失败: " . $e->getMessage() . "\n";
            return [];
        }
    }

    /**
     * 处理业务
     * @param array $clients 所有在线用户数
     * @param string $key 当前用户的key 通过 $clients[$key] 可以拿到当前用户的 ws实例
     * @param string $msg 当前用户发来的消息
     * @return void
     */
    public function logic(array $clients, string $key, string $msg): void
    {
        // 仅检查消息是否为UTF-8编码，完全移除长度限制
        $logMsg = $msg;
        $isValidMsg = true;
        
        // 只检查是否为有效的UTF-8编码
        if (!mb_check_encoding($msg, 'UTF-8')) {
            $logMsg = "非UTF-8编码数据";
            $isValidMsg = false;
            echo date('Y-m-d H:i:s') . " 检测到非UTF-8编码数据\n";
        } else if (strlen($msg) > 1000) {
            // 只在日志中缩短显示，但不拒绝处理任何长度的消息
            $logMsg = substr($msg, 0, 100) . "...(消息过长，日志中省略)";
        }
        
        echo date('Y-m-d H:i:s') . " 在线人数【" . count($clients) . "】|老用户[key:{$key}|请求信息长度:" . strlen($msg) . "字节]\n";
        
        // 如果消息编码无效，返回错误给客户端
        if (!$isValidMsg) {
            if (isset($clients[$key]) && $clients[$key] instanceof Socket) {
                echo date('Y-m-d H:i:s') . " 发送错误响应给客户端，要求重发数据\n";
                $errorResponse = json_encode([
                    'event' => 'error',
                    'code' => 1001,
                    'message' => '消息编码错误，请使用UTF-8编码',
                    'action' => 'resend'
                ], JSON_UNESCAPED_UNICODE);
                $this->send($clients[$key], $errorResponse);
            }
            return;
        }
        
        try {
            $message = json_decode($msg, true);
            
            // 检查JSON解析是否成功
            if (json_last_error() !== JSON_ERROR_NONE) {
                $errorMsg = "JSON解析失败: " . json_last_error_msg();
                echo date('Y-m-d H:i:s') . " " . $errorMsg . "\n";
                
                // 尝试记录原始消息的一部分以便调试
                $debugMsg = strlen($msg) > 100 ? substr($msg, 0, 100) . "..." : $msg;
                echo date('Y-m-d H:i:s') . " 原始消息部分内容: " . $debugMsg . "\n";
                
                // 发送JSON解析错误给客户端
                if (isset($clients[$key]) && $clients[$key] instanceof Socket) {
                    $errorResponse = json_encode([
                        'event' => 'error',
                        'code' => 1002,
                        'message' => $errorMsg,
                        'action' => 'resend'
                    ], JSON_UNESCAPED_UNICODE);
                    $this->send($clients[$key], $errorResponse);
                }
                return;
            }
            
            if (isset($message['event'])) {
                $resp = match ($message['event']) {
                    "ping" => ['to' => $clients[$key], 'message' => ''],
                    "join" => $this->eventJoin($message, $key, $clients),
                    "send" => $this->eventSend($message, $key, $clients),
                    "history" => $this->eventHistory($message, $key, $clients),
                    default => $this->eventJson($message, $key, $clients),
                };
            } else {
                $resp = $this->eventStr($msg, $key, $clients);
            }
        } catch (Exception $e) {
            echo date('Y-m-d H:i:s') . " 消息处理异常: " . $e->getMessage() . "\n";
            
            // 发送异常错误给客户端
            if (isset($clients[$key]) && $clients[$key] instanceof Socket) {
                $errorResponse = json_encode([
                    'event' => 'error',
                    'code' => 1003,
                    'message' => '服务器处理消息时发生异常，请重试',
                    'action' => 'resend'
                ], JSON_UNESCAPED_UNICODE);
                $this->send($clients[$key], $errorResponse);
            }
            return;
        }
        
        if (empty($resp)) return;

        // 处理公共消息
        $to = $resp['to'] ?? false;
        $response = $resp['message'];
        switch (true) {
            case is_array($to) :
                foreach ($to as $client) {
                    $this->send($client, $this->stripTags($response));
                }
                break;
            case $to instanceof Socket :
                $this->send($to, $this->stripTags($response));
                break;
            case $to == 'all' :
                foreach ($this->users as $client) {
                    $this->send($client, $this->stripTags($response));
                }
                break;
            default :
                break;
        }
    }
    
    /**
     * 处理获取历史消息请求
     * @param array $message 消息数据
     * @param string $key 用户标识
     * @param array $clients 客户端列表
     * @return array 响应数据
     */
    public function eventHistory($message, $key, $clients): array
    {
        $start = isset($message['start']) ? intval($message['start']) : 0;
        $count = isset($message['count']) ? intval($message['count']) : 5;
        $nickname = $message['nickname'] ?? '';
        
        echo date('Y-m-d H:i:s') . " 处理历史消息请求 - start: {$start}, count: {$count}, nickname: {$nickname}\n";
        
        $messages = $this->getHistoryMessages($start, $count);
        
        // 获取消息总数，用于前端判断是否还有更多消息
        $totalCount = 0;
        if ($this->redis) {
            $totalCount = $this->redis->lLen($this->chatHistoryKey);
        }
        
        $response = [
            'event' => 'history',
            'messages' => $messages,
            'start' => $start,
            'count' => count($messages),
            'total' => $totalCount,
            'nickname' => $nickname
        ];
        
        echo date('Y-m-d H:i:s') . " 返回历史消息 - total: {$totalCount}, 返回消息数: " . count($messages) . "\n";
        
        return [
            'to' => $clients[$key],
            'message' => json_encode($response, JSON_UNESCAPED_UNICODE)
        ];
    }

    public function stripTags($str): string
    {
        $allowed_tags = ['span', 'p', 'strong', 'em', 'br', 'ul', 'ol', 'li'];
        // 确保所有HTML标签都是正确闭合的
        return strip_tags($str, '<' . implode('><', $allowed_tags) . '>');
    }

    public function eventJoin($message, $key, $clients): bool
    {
        $nickname = $message['nickname'];
        $this->users[$nickname] = $clients[$key];
        foreach ($this->users as $nick => $client) {
            if ($nick == $nickname) {
                $pre = "[<span class='name' style='color: purple'>我</span>]";
            } else {
                $pre = "[<span class='name' style='color: blue'>{$message['nickname']}</span>]";
            }
            $response = "{$pre}加入了聊天,当前在线:[" . count($this->users) . "]";
            $this->send($client, $this->stripTags($response));
        }
        return false;
    }

    public function eventSend($message, $key, $clients): bool
    {
        $nickname = $message['nickname'];
        $responses = [];
        
        foreach ($this->users as $nick => $client) {
            if ($nick == $nickname) {
                $pre = "[<span class='name' style='color: purple'>我</span>]";
            } else {
                $pre = "[<span class='name' style='color: blue'>{$message['nickname']}</span>]";
            }
            $response = "{$pre}：{$message['msg']}";
            $responses[$nick] = $this->stripTags($response);
            $this->send($client, $responses[$nick]);
        }
        
        // 存储到历史记录使用简单的文本格式：nickname:message
        $historyMsg = "{$message['nickname']}:{$message['msg']}";
        $this->addMessageToHistory($historyMsg);
        
        return false;
    }

    public function eventJson($message, $key, $clients): array
    {
        if (isset($message['final_from_name']) && isset($message['msg']) && !is_array($message['msg'])) {
            //$this->eventJoin(['event' => 'join', 'nickname' => $message['final_from_name']], $key, $clients);
            $nickname = strip_tags($message['final_from_name']);
            $msg1 = strip_tags($message['msg']);
            $msg = "[<span style='background-color: green'>微信</span>][<span class='name' data-nickname='$nickname' style='color: blue'>$nickname</span>]：{$msg1}";
            
            // 添加到历史记录（简单文本格式）
            $historyMsg = "{$nickname}:{$msg1}";
            $this->addMessageToHistory($historyMsg);
            
            return ['to' => "all", "message" => $msg];
        } else {
            return ['to' => $clients[$key], "message" => json_encode($message, JSON_UNESCAPED_UNICODE)];
        }
    }

    public function eventStr($message, $key, $clients): array
    {
        // 添加到历史记录
        //$this->addMessageToHistory($message);
        
        return ['to' => "all", "message" => $message];
    }

    /**
     * 创建socket服务 并listen IP和Port
     * @throws Exception
     */
    public function service(): void
    {
        try {
            $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
            socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);

            if ($socket === false) {
                $error_code = socket_last_error();
                throw new Exception("Couldn't create socket: [$error_code] " . socket_strerror($error_code) . " \n");
            }
            socket_bind($socket, $this->address, $this->port);
            socket_listen($socket, $this->port);
            echo date('Y-m-d H:i:s') . " listen on ws://$this->address:$this->port ... \n";
            $this->_socket = $socket;
        }catch (Exception $e){
            echo $e->getMessage();
            die;
        }
    }

    /**
     * 堵塞方式(死循环)启动 websocket 服务，等待客户端连入。
     * @return void
     */
    public function run(): void
    {
        try {
            $this->service();
            $clients['ws'] = $this->_socket;
            while (true) {
                $changes = $clients;
                $write = NULL;
                $except = NULL;
                socket_select($changes, $write, $except, NULL);
                try {
                    // 检查Redis连接并执行心跳
                    $this->checkRedisConnection();
                    
                    foreach ($changes as $key => $_socket) {
                        if ($this->_socket == $_socket) { //判断是不是新接入的socket
                            if (($newClient = socket_accept($_socket)) === false) {
                                throw new Exception('failed to accept socket: ' . socket_strerror(socket_last_error()) . "\n");
                            }
                            
                            // 读取客户端发送的握手数据
                            $handshakeData = socket_read($newClient, 32768);
                            if ($handshakeData === false) {
                                echo date('Y-m-d H:i:s') . " 读取握手数据失败\n";
                                continue;
                            }
                            
                            // 处理握手
                            $line = trim($handshakeData);
                            $headers = $this->handshakeAndGetHeaders($newClient, $line);
                            if (!$headers) {
                                echo date('Y-m-d H:i:s') . " 握手失败\n";
                                continue;
                            }
                            
                            // 获取客户端IP
                            socket_getpeername($newClient, $ip);
                            
                            // 记录连接信息
                            $secWebSocketKey = $headers['Sec-WebSocket-Key'] ?? '未知';
                            $clients[$secWebSocketKey] = $newClient;
                            echo date('Y-m-d H:i:s') . " 在线人数【" . count($clients) . "】|新用户[ip:{$ip}|swk:{$secWebSocketKey}]\n";
                        } else {
                            $len = socket_recv($_socket, $buffer, 32768, 0);
                            if ($len < 7 || ($len == 8 && strlen($this->message($buffer)) == 2)) {
                                if ($key != 'ws') {
                                    unset($clients[$key]);
                                    foreach ($this->users as $nickname => $socket) {
                                        if ($_socket == $socket) unset($this->users[$nickname]);
                                    }
                                }
                                continue;
                            }
                            
                            // 处理消息
                            $msg = $this->message($buffer);
                            $this->logic($clients, $key, $msg);
                        }
                    }
                } catch (Exception $e) {
                    echo date('Y-m-d H:i:s') . " 错误: " . $e->getMessage() . "\n";
                }
            }
        } catch (Exception $e) {
            echo date('Y-m-d H:i:s') . " 服务错误: " . $e->getMessage() . "\n";
        }
    }

    /**
     * 握手并获取客户端的头信息
     * @param Socket $newClient socket客户端实例
     * @param string $info socket_read 的数据
     * @return array|false 接收到的信息
     */
    public function handshakeAndGetHeaders(Socket $newClient, string $info): false|array
    {

        $lines = preg_split("/\r\n/", $info);
        foreach ($lines as $line) {
            $line = chop($line);
            if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
                $headers[$matches[1]] = $matches[2];
            }
        }
        if (!isset($headers['Sec-WebSocket-Key'])) return false;

        $sec_key = $headers['Sec-WebSocket-Key'];
        $sec_accept = base64_encode(sha1($sec_key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
        $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
            "Upgrade: websocket\r\n" .
            "Sec-WebSocket-Version: 13\r\n" .
            "Connection: Upgrade\r\n" .
            "Date: " . gmdate("D, d M Y H:i:s") . " GMT\r\n" .
            "Server: PHP " . phpversion() . "/websocketServer\r\n" .
            "WebSocket-Origin: $this->address\r\n" .
            "WebSocket-Location: ws://{$headers['Host']}\r\n" .
            "Sec-WebSocket-Accept:$sec_accept\r\n\r\n";
        if (socket_write($newClient, $upgrade, strlen($upgrade))) return $headers;

        return false;
    }

    /**
     * 解析接收的数据
     * @param $buffer
     * @return string
     */
    public function message($buffer): string
    {
        $decoded = "";
        if (empty($buffer)) return $decoded;
        $len = ord($buffer[1]) & 127;
        if ($len === 126) {
            $masks = substr($buffer, 4, 4);
            $data = substr($buffer, 8);
        } else {
            if ($len === 127) {
                $masks = substr($buffer, 10, 4);
                $data = substr($buffer, 14);
            } else {
                $masks = substr($buffer, 2, 4);
                $data = substr($buffer, 6);
            }
        }
        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }
        return $decoded;
    }

    /**
     * 发送数据
     * @param Socket $client 新接入的socket
     * @param string $msg 要发送的数据
     * @return void
     */
    public function send(Socket $client, string $msg): void
    {
        $msg = $this->dataFrame($msg);
        $lens = strlen($msg);//总长度
        @socket_write($client, $msg, $lens);
        return;
//        $finish = 0;//已经写入完成的长度
//        while ($finish < $lens) {
//            $bytes = @socket_write($client, substr($msg, $finish));
//            if ($bytes === false) {
//                echo date('Y-m-d H:i:s') . " error：Failed to write to socket.\r\n";
//                return;
//            }
//            $finish += $bytes;
//        }
    }

    /**
     * 构建相应的 WebSocket 数据帧
     * @param $msg
     * @return string
     */
    public function dataFrame($msg): string
    {
        $len = strlen($msg);
        if ($len <= 125) {
            return "\x81" . chr($len) . $msg;
        } elseif ($len <= 65535) {
            return "\x81\x7E" . pack('n', $len) . $msg;
        } else {
            return "\x81\x7F" . pack('J', $len) . $msg;
        }
    }
}
